/**
 * @(#)RefleccionClases.java Dec 27, 2006
 *
 * Copyright (c) 2005-2010 Psiqueware S.A. de C.V. All Rights Reserved.
 * This software is the proprietary information Psiqueware S.A. de C.V.
 * Use is subject to license terms.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
 * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 *
 * <br/>Usuario : AngelElectrico
 * <br/>Creaci&oacute;n: Dec 27, 2006 - 5:06:02 PM
 * <br/>
 * <br/>
 * <br/><t5>Cambios :</t5>
 * <br/><b>Fecha | Autor  |  Cambios</b>
 * <ul>
 * <li>Dec 27, 2006 | <a href = "mailto:AngelElectrico@PsiqueWare.com">Angel El&eacute;ctrico</a> | Creaci&oacute;n del Objeto.</li>
 * <li>Sep 25, 2008 | <a href = "mailto:AngelElectrico@PsiqueWare.com">Angel El&eacute;ctrico</a> | Se agregan nuevos m&eacute;todos.</li>
 *
 * </ul>
 */

package com.psiqueware.alheli.reflection;

import com.psiqueware.alheli.logging.Logger;
import com.psiqueware.alheli.logging.LoggerFactory;

import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.sql.Timestamp;

/**
 * M&ntilde;etodos utilitarios para obtener informaci&oacute;n de la clase de un bean por reflexi&oacute;n.
 * <br/>Paquete Original: com.psiqueware.basico.reflection
 * <br/>Nombre  Original: RefleccionClases
 * <br/>Oprima para ver : <a href="RefleccionClases.java.html"><i>C&oacute;digo Fuente</i> </a>
 *
 * @author <a href="mailto:AngelElectrico@PsiqueWare.com">Angel El&eacute;ctrico</a>
 * @version Revision: 1.1 Dec 27, 2006 5:06:02 PM
 */
public final class RefleccionClases
{

    /**
     * s_logger de clase para RefleccionClases
     */
    private static final Logger s_log = LoggerFactory.getLogger(RefleccionClases.class);

    private static final Class[] CLASS_ARRAY_VACIO = new Class[]{};


    /**
     * Constructor de RefleccionClases.
     */
    public RefleccionClases()
    {

    }

    /**
     * Devuelve una copia de un pojo. Solo se copian los valores de los
     * atributos que son tienen un getter y un setter publico.
     *
     * @param original objeto a clonar.
     *
     * @return un objeto clonado.
     */
    public static Object cloneMe(Object original)
    {
        if (null == original)
            throw new IllegalArgumentException("<error>El argumento 'original' no puede ser NULL.</error>");

        //Listado con los objetos que ya se han obtenido
        // de este bean. nos permite evitar el problema
        // que se presenta
        Class classOriginal = original.getClass();
        Object newInstance = nuevaInstancia(classOriginal);
        String nombreMetodo = null;

        if (null == newInstance)
        {
            throw new RuntimeException("<error>El objeto no tiene un constructor publico sin argumentos.</error>");
        }
        //Copiamos los valores de todos los getters publicos en los setters publicos.
        try
        {
            Method[] methods = classOriginal.getMethods();
            for (Method metodoGetter : methods)
            {
                //Verificamos que el metodo sea un getter accesible.
                if (metodoGetter.getName().startsWith("get") && Modifier.isPublic(metodoGetter.getModifiers()))
                {
                    //Verificamos que tenga un setter
                    nombreMetodo = metodoGetter.getName();
                    String nombreAtributo = nombreMetodo.substring(3, nombreMetodo.length());
                    String nombreMetodoSetter = "set" + nombreAtributo;
                    //Existe el setter?
                    if (RefleccionMetodos.isMetodoExistente(classOriginal, nombreMetodoSetter))
                    {
                        Method metodoSetter = classOriginal.getMethod(nombreMetodoSetter, metodoGetter.getReturnType());
                        //es publico?
                        if (Modifier.isPublic(metodoSetter.getModifiers()))
                        {
                            //Ya tenemos un getter y un setter publicos, esta clase califica.
                            Object valorAtributo = metodoGetter.invoke(original);
                            metodoSetter.invoke(newInstance, valorAtributo);
                        }
                    }
                }
            }
        }
        catch (NoSuchMethodException e)
        {
            final String error = "<error>No exist&iacute;a el metodo setter buscado.</error>";
            s_log.error(error);
            s_log.error(e);
            throw new RuntimeException(error, e);
        }
        catch (IllegalAccessException e)
        {
            final String error = "<error>No se pudo accesar al metodo setter no era publico.</error>";
            s_log.error(error);
            s_log.error(e);
            throw new RuntimeException(error, e);
        }
        catch (InvocationTargetException e)
        {
            final StackTraceElement[] stackTraceElements = e.getTargetException().getStackTrace();
            StringBuilder stb = new StringBuilder(stackTraceElements.length * 125);
            for (StackTraceElement stackTraceElement : stackTraceElements)
            {
                stb.append(stackTraceElement.toString()).append("\n");
            }

            final String error = "<error>Se lanz&oacute; una excepci&oacute;n al invocar el m&eacute;todo:" + nombreMetodo + ", Causa:" + e.getTargetException() + ", Msg:" + e.getTargetException().getMessage() + ", Trace:\n" + stb + "</error>";
            s_log.error(error);
            s_log.error(e);
            throw new RuntimeException(error, e);
        }
        return newInstance;
    }

    /**
     * Construye una nueva instancia de la clase dada. Esta clase debe tener un constructor publico sin argumentos.
     *
     * @param nombreClase nombre calificado de la Clase que se desea instanciar.
     *
     * @return Object con la nueva instancia de la clase.
     */
    public static Object nuevaInstancia(String nombreClase)
    {
        return nuevaInstancia(nombreClase, new Object[]{});
    }

    /**
     * Construye una nueva instancia de la clase dada. Esta clase debe tener un constructor publico sin argumentos.
     *
     * @param classOriginal Clase que se desea instanciar.
     *
     * @return Object con la nueva instancia de la clase, si tiene un constructor p&uacute;blico; <code>null</code> en caso contrario.
     */
    public static Object nuevaInstancia(Class classOriginal)
    {
        return nuevaInstancia(classOriginal, new Object[]{});
    }


    /**
     * Construye una nueva instancia de la clase dada. Esta clase debe tener un constructor publico sin argumentos.
     *
     * @param nombreClase nombre calificado de la Clase que se desea instanciar.
     * @param objects     argumentos del constructor.
     *
     * @return Object con la nueva instancia de la clase.
     */
    public static Object nuevaInstancia(String nombreClase, Object[] objects)
    {
        try
        {
            if (null == nombreClase)
                throw new IllegalArgumentException("<error>El argumento 'nombreClase' no puede ser NULL.</error>");
            if (0 == nombreClase.trim().length())
                throw new IllegalArgumentException("<error>El argumento 'nombreClase' es una cadena vacia.</error>");

            final Class aClass = Class.forName(nombreClase.trim());
            return nuevaInstancia(aClass, objects);
        }
        catch (ClassNotFoundException e)
        {
            final String error = "<error><msg>No se encontr&oacute; la clase en el contexto</msg><parametros>" + nombreClase + "</parametros></error>";
            System.out.println(error);
            System.out.println(e);
            throw new RuntimeException(error, e);
        }
    }

    /**
     * Construye una nueva instancia de la clase dada. Esta clase debe tener un constructor publico sin argumentos.
     *
     * @param classOriginal Clase que se desea instanciar.
     * @param objects       argumentos del constructor.
     *
     * @return Object con la nueva instancia de la clase, si tiene un constructor p&uacute;blico; <code>null</code> en caso contrario.
     */
    public static Object nuevaInstancia(Class classOriginal, Object[] objects)
    {
        if (null == classOriginal)
            throw new IllegalArgumentException("<error>El argumento 'classOriginal' no puede ser NULL.</error>");

        Object newInstance = null;
        //Intentamos construir una nueva instancia para almacenar nuestros datos.
        try
        {
            Constructor[] constructors = classOriginal.getConstructors();
            for (final Constructor constructor : constructors)
            {
                //Verificamos que tenga un constructor publico y que sus argumentos sean los mismos
                if (Modifier.isPublic(constructor.getModifiers()) && constructor.getParameterTypes().length == objects.length && comparaArgumentosConstructor(constructor, objects))
                {
                    newInstance = constructor.newInstance(objects);
                }
            }
        }
        catch (InstantiationException e)
        {
            final String error = "<error>No se pudo instanciar la clase para :" + classOriginal + "</error>";
            System.out.println(error);
            System.out.println(e);
            throw new RuntimeException(error, e);
        }
        catch (IllegalAccessException e)
        {
            final String error = "<error>No se puede accesar al constructor para la clase de:" + classOriginal + "</error>";
            System.out.println(error);
            System.out.println(e);
            throw new RuntimeException(error, e);
        }
        catch (InvocationTargetException e)
        {
            final String error = "<error>El constructor de la clase " + classOriginal + " a soltado una excepci&oacute;n.</error>";
            System.out.println(error);
            System.out.println(e);
            throw new RuntimeException(error, e);
        }
        if (null == newInstance)
            throw new IllegalArgumentException("<error>A fallado una postcondici&oacute;n. El argumento de salida 'newInstance' no puede ser NULL para la clase =" + classOriginal + ".</error>");

        return newInstance;
    }

    /**
     * Compara los argumentos de un constructor de manera que devolver&aacute; verdadero si el arreglo de objetos
     * proporcionado puede utilizarse para instanciar el constructor.
     *
     * @param constructor a instanciar.
     * @param objects     argumentos a proporcionarle.
     *
     * @return true si se puede instanciar la clase del constructor a partir de estos objetos; false en caso contrario.
     */
    private static boolean comparaArgumentosConstructor(Constructor constructor, Object[] objects)
    {
        Class[] parameterTypes = constructor.getParameterTypes();
        boolean mismosParametros = true;
        for (int j = 0; j < objects.length; j++)
        {
            Object object = objects[j];
            mismosParametros &= parameterTypes[j].isAssignableFrom(object.getClass());
        }
        return mismosParametros;
    }


    /**
     * Busca un metodo apartir del nombre de este y devuelve el valor de su returns.
     *
     * @param invocaEnMi    objeto donde se busca el metodo.
     * @param metodoBuscado nombre del metodo a buscar.
     *
     * @return valor devuelto por el metodo.
     *
     * @throws IllegalAccessException    en caso de no poder acceder al m&eacute;todo.
     * @throws NoSuchMethodException     en caso de que no exista el m&eacute;todo.
     * @throws InvocationTargetException en caso de que el m&eacute;todo suelte una excepci&oacute;n al invocarse.
     */
    public static Object invocaMetodoEnMi(Object invocaEnMi, String metodoBuscado) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException
    {
        if (null == invocaEnMi)
            throw new IllegalArgumentException("<error>El argumento 'invocaEnMi' no puede ser NULL.</error>");
        if (null == metodoBuscado)
            throw new IllegalArgumentException("<error>El argumento 'metodoBuscado' no puede ser NULL.</error>");
        if (0 == metodoBuscado.trim().length())
            throw new IllegalArgumentException("<error>El argumento 'metodoBuscado' no puede encontrarse vacio.</error>");

        final Method metodoValor = invocaEnMi.getClass().getMethod(metodoBuscado, CLASS_ARRAY_VACIO);
        return metodoValor.invoke(invocaEnMi, CLASS_ARRAY_VACIO);
    }

    /**
     * Devuelve verdadero si una clase es derivada de un primitivo, un wrapper, un Date o un String.
     *
     * @param clazz clase a examinar.
     *
     * @return <code>true</code> si es una clase de los tipos enumerados; <code>false</code> en caso contrario.
     *
     * @noinspection OverlyComplexMethod
     */
    public static boolean isClaseBasica(final Class clazz)
    {
        boolean salida = false;
        if (clazz == Serializable.class)
        {salida = false;}
        else if (clazz.isPrimitive() && !clazz.isAssignableFrom(Void.class))
        {salida = true;}
        else if (clazz.isAssignableFrom(Character.class))
        {salida = true;}
        else if (clazz.isAssignableFrom(Short.class))
        {salida = true;}
        else if (clazz.isAssignableFrom(Byte.class))
        {salida = true;}
        else if (clazz.isAssignableFrom(Integer.class))
        {salida = true;}
        else if (clazz.isAssignableFrom(Long.class))
        {salida = true;}
        else if (clazz.isAssignableFrom(Float.class))
        {salida = true;}
        else if (clazz.isAssignableFrom(Boolean.class))
        {salida = true;}
        else if (clazz.isAssignableFrom(Double.class))
        {salida = true;}
        else if (clazz.isAssignableFrom(String.class))
        {salida = true;}
        else if (clazz.isAssignableFrom(java.util.Date.class))
        {salida = true;}
        else if (clazz.isAssignableFrom(java.sql.Date.class))
        {salida = true;}
        else if (clazz.isAssignableFrom(Timestamp.class))
        {salida = true;}
        else if (clazz.isAssignableFrom(BigDecimal.class))
        {salida = true;}
        else if (clazz.isAssignableFrom(BigInteger.class))
        {salida = true;}
        else if (clazz.isEnum())
        {salida = true;}
        return salida;
    }

    /**
     * Busca el {@link Field} a partir de su nombre.
     *
     * @param clazz  cuyos {@link Field}s se van a examinar.
     * @param nombre del atributo.
     *
     * @return {@link Field} con el nombre dado; <code>null</code> en caso de no encontrarlo.
     */
    public static Field buscaFieldPorNombre(final Class clazz, final String nombre)
    {
        Field salida = null;
        final String nombreConM = nombre.startsWith("m_") ? nombre : "m_" + nombre;
        final Field[] fields = clazz.getDeclaredFields();
        for (final Field field : fields)
        {
            final String fieldName = field.getName();
            if (fieldName.equals(nombre) || fieldName.equals(nombreConM))
            {
                salida = field;
            }
        }
        return salida;
    }

}

